package datastore.db;

import java.util.Arrays;

import org.apache.commons.codec.binary.Base64;

import datastore.db.exception.BadKeyError;
import datastore.api.impl.serializer.JsonSerializer;

/**
 Every model instance that has been stored in the datastore has a unique key that represents the object. The key() method of a model instance returns the Key object for the instance. If the instance has never been put() in the datastore, key() raises a NotSavedError.

 An application can retrieve a model instance for a given Key using the get() function.

 Key instances can be values for datastore entity properties, including Expando dynamic properties and ListProperty members. The ReferenceProperty model provides features for Key property values such as automatic dereferencing.
 */
public class Key {
	private static final JsonSerializer json = new JsonSerializer();
	
    protected String app;
    protected Key parent;
    protected Object id;
    protected Class<? extends Model> kind;
    
    private Key() { }
    
    /**
     *  A unique key for a datastore object.
     *  A key can be encoded to a string by passing the Key object to str() (or calling the object's __str__() method). A string-encoded key is an opaque value using characters safe for including in URLs. The string-encoded key can be converted back to a Key object by passing it to the Key constructor (the encoded argument).
     *  Note: A string-encoded key can be converted back to the raw key data. This makes it easy to guess other keys when one is known. While string-encoded key values are safe to include in URLs, an application should only do so if key guessability is not an issue.
     * @param encoded The str form of a Key instance to convert back into a Key.
     */
    public Key(String encoded) {
        Key decoded = decode(encoded.getBytes());
        this.id = decoded.id;
        this.app = decoded.app;
        this.parent = decoded.parent;
        this.kind = decoded.kind;
    }

    /**
     * Returns the numeric ID of the data entity, as Long, or null if the entity does not have a numeric ID.
     * @return the numeric ID of the data entity, as Long, or null if the entity does not have a numeric ID.
     */
    public Long getId() {
        if (id instanceof Long) {
            return (Long) id;
        }
        return null;
    }

    /**
     * Returns the name of the data entity, or null if the entity does not have a name.
     * @return the name of the data entity, or null if the entity does not have a name.
     */
    public String getName() {
        if (id instanceof String) {
            return (String) id;
        }
        return null;
    }

    /**
     * Returns the name of the application that stored the data entity.
     * @return the name of the application that stored the data entity.
     */
    public String getApp() {
        return app;
    }

    /**
     * Returns the Key of the data entity's parent entity, or null if the entity has no parent.
     * @return the Key of the data entity's parent entity, or null if the entity has no parent.
     */
    public Key getParent() {
        return parent;
    }
    
    /**
     * Returns the kind of the data entity, as a string.
     * @return the kind of the data entity, as a string.
     */
    public Class<? extends Model> getKind() {
        return kind;
    }
    
    /**
     * Returns the name or numeric ID of the data entity, whichever it has, or null if the entity has neither a name nor a numeric ID.
     * @return the name or numeric ID of the data entity, whichever it has, or null if the entity has neither a name nor a numeric ID.
     */
    public Object getIdOrName() {
        return id;
    }
    
    /**
     * Returns true if the entity has either a name or a numeric ID.
     * @return true if the entity has either a name or a numeric ID.
     */
    public boolean hasIdOrName() {
        return id != null;
    }
    
    @Override
    public String toString() {
        return new String(encode());
    }

	Key decode(byte[] encoded) {
		try {
			//TODO: move to protobuf
			return (Key) json.fromBytes(Base64.decodeBase64(encoded));
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	byte[] encode() {
		//TODO: move to protobuf
		return Base64.encodeBase64(json.toBytes(this));
	}
    
    public static Key fromPath(Object ... args) {
        return fromPath(null, args);
    }

    /**
     *  Builds a new Key object from an ancestor path of one or more entity keys.

 A path represents the hierarchy of parent-child relationships for an entity. Each entity in the path is represented the entity's kind, and either its numeric ID or its key name. The full path represents the entity that appears last in the path, with its ancestors (parents) as preceding entities.

 For example, the following call creates a key for an entity of kind Address with the numeric ID 9876 whose parent is an entity of kind User with the named key 'Boris':

 k = Key.fromPath(User.class, 'Boris', Address.class, 9876)

 For more information about paths, see Keys and Entity Groups.

     * @param parentKey specifies a parent entity to prepend to the given path. Its value is the parent's Key.
     * @param args The path from the root entity to the subject. Each entity in the path is represented by two elements in the list: the name of the kind, and the key name or ID of the entity of that kind.
 
     * @return
     */
    @SuppressWarnings("unchecked")
    public static Key fromPath(Key parentKey, Object ... args) {
        if (args.length < 2 || args.length % 2 != 0) {
            throw new BadKeyError("wrong number of arguments to from path: "+args.length);
        }
        Key k = new Key();
        k.app = Datastore.getAppName();
        
        //parse idOrName
        Object top = args[args.length - 1];
        if (top instanceof Long || top instanceof String) {
            k.id = top;
        } else {
            throw new BadKeyError("invalid type for name or id: "+top.getClass());
        }
        
        //parse kind
        Object classObj = args[args.length - 2];
        if (classObj instanceof String) {
            String className = (String) classObj;
            try {
                k.kind = (Class<? extends Model>) Class.forName(className);
            } catch (ClassNotFoundException e) {
                throw new BadKeyError(e);
            }
        } else {
            k.kind = (Class<? extends Model>) classObj;
        }
        
        // determin parent
        if (args.length > 2) {
            // get parent key
            k.parent = fromPath(parentKey, Arrays.copyOfRange(args, 0, args.length - 2));
        } else if (args.length == 2 && parentKey != null) {
            //prepend passed in key
            k.parent = parentKey;
        }
        
        return k;
    }
}
